/*
 * JBoss, Home of Professional Open Source Copyright 2009, Red Hat Middleware
 * LLC, and individual contributors by the @authors tag. See the copyright.txt
 * in the distribution for a full listing of individual contributors.
 * 
 * This is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 * 
 * This software is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this software; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
 * site: http://www.fsf.org.
 */
package org.fing.edu.uy.esbadp.action.transform;

import java.io.StringReader;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;

import javax.xml.XMLConstants;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.fing.edu.uy.esbadp.assertion.AssertProperty;
import org.fing.edu.uy.esbadp.core.AdpFlowConstants;
import org.fing.edu.uy.esbadp.core.AdpFlowInterface;
import org.fing.edu.uy.esbadp.core.AdpFlowTRN;
import org.fing.edu.uy.esbadp.core.AdpFlowTree;
import org.fing.edu.uy.esbadp.core.entity.GenericTreeNode;
import org.fing.edu.uy.esbadp.logger.LoggerADP;
import org.fing.edu.uy.esbadp.utils.StringUtils;
import org.jboss.internal.soa.esb.assertion.AssertArgument;
import org.jboss.soa.esb.ConfigurationException;
import org.jboss.soa.esb.actions.AbstractActionPipelineProcessor;
import org.jboss.soa.esb.actions.ActionLifecycleException;
import org.jboss.soa.esb.actions.ActionProcessingException;
import org.jboss.soa.esb.actions.transformation.xslt.ResultFactory;
import org.jboss.soa.esb.actions.transformation.xslt.ResultFactory.ResultType;
import org.jboss.soa.esb.actions.transformation.xslt.SourceFactory;
import org.jboss.soa.esb.actions.transformation.xslt.SourceResult;
import org.jboss.soa.esb.actions.transformation.xslt.TransformerFactoryConfig;
import org.jboss.soa.esb.actions.transformation.xslt.TransformerFactoryConfig.Builder;
import org.jboss.soa.esb.helpers.ConfigTree;
import org.jboss.soa.esb.listeners.message.MessageDeliverException;
import org.jboss.soa.esb.message.Message;
import org.jboss.soa.esb.message.MessagePayloadProxy;
import org.jboss.soa.esb.util.ClassUtil;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.SAXParseException;

public class TranformAction extends AbstractActionPipelineProcessor {

	private static java.util.logging.Logger logger = LoggerADP.getLogger(TranformAction.class.getSimpleName());
    
	private boolean failOnWarning;
    private boolean validation;
    private SAXParserFactory validationFactory;
    private String schemaFile;
    private String schemaLanguage;
    private MessagePayloadProxy payloadProxy;
    
    //By Esb-Adaptative
    private int maxTranformerPerTemplate;
    private Map<String, Templates> mapTemplates;
    private Map<String, ArrayBlockingQueue<Transformer>> mapTransformers;
    private Map<String, Date> timeStampTemplates;
    private TimeoutChecker _timeoutChecker = null;
    
    private TransformerFactoryConfig transformerConfig;
	
	public TranformAction(){
    	
    }

	/**
     * Creates the XsltAction instance.
     * 
     * @throws ConfigurationException if the config is incorrect.
     * 
     */
    public TranformAction(final ConfigTree config) throws ConfigurationException {
        transformerConfig = createConfig(config);
        failOnWarning = config.getBooleanAttribute("failOnWarning", true);
        validation = config.getBooleanAttribute("validation", false);
        schemaFile = config.getAttribute("schemaFile");
        schemaLanguage = config.getAttribute("schemaLanguage");
        payloadProxy = new MessagePayloadProxy(config);
        
        //By Esb-Adaptative
        maxTranformerPerTemplate = Integer.parseInt(config.getAttribute("maxTranformerPerTemplate", "1"));
        mapTransformers = Collections.synchronizedMap(new HashMap<String, ArrayBlockingQueue<Transformer>>());
        mapTemplates = Collections.synchronizedMap(new HashMap<String, Templates>());
        timeStampTemplates = Collections.synchronizedMap(new HashMap<String, Date>());
    }
    
    
    /**
     * Initialise the action instance.
     * <p/>
     * This method is called after the action instance has been instantiated so that
     * configuration options can be validated.
     * 
     * @throws ActionLifecycleException for errors during initialisation.
     */
    public void initialise() throws ActionLifecycleException {
        _timeoutChecker = new TimeoutChecker();
        _timeoutChecker.start();
    }

    /**
     * Destroy the action instance.
     * <p/>
     * This method is called prior to the release of the action instance.  All
     * resources associated with this action instance should be released as the
     * instance will no longer be used.
     */
    public void destroy() throws ActionLifecycleException{
        _timeoutChecker.terminate();
        _timeoutChecker = null ;
    }
    
    /**
     * Performs the xsl transformation of the message payload.
     * 
     * @param message The ESB {@link Message} object whose payload should be transformed.
     *                The payload is extracted and set using contract specified by {@link MessagePayloadProxy}.
     *      
     * @return {@link Message} The same ESB Message instance passed-in but with it payload transformed.
     * @throws ActionProcessingException if an error occurs while trying to perform the transformation.
     */
    public Message process(final Message message) throws ActionProcessingException {
    	
    	if(logger.isLoggable(LoggerADP.DEBUG_LEVEL)){
			logger.log(LoggerADP.DEBUG_LEVEL, "Process Action -> {0}", this.getClass().getName());
		}
    	
    	AssertArgument.isNotNull(message, "message");
		AssertProperty.isNotNull(message, AdpFlowConstants.PropertyFlowServices);
		
        try
        {   
        	//se obtiene el arbol con las acciones a realizar
    		AdpFlowTree adpFlowTree = (AdpFlowTree) message.getProperties().getProperty(AdpFlowConstants.PropertyFlowServices);
    		GenericTreeNode<AdpFlowInterface> treeNode = adpFlowTree.getCurrent();
        			
    		//como esta accion no tiene que decidir el proximo nodo es el primer hijo
    		GenericTreeNode<AdpFlowInterface> nextCurrent = treeNode.getChildren().size() > 0 ? treeNode.getChildAt(0) : null;
    		adpFlowTree.setCurrent(nextCurrent);
    		
    		AdpFlowTRN adpFlowAction = (AdpFlowTRN) treeNode.getData();
    		//initiliaze file template xsl to transform message
    		initializeTemplateFile(adpFlowAction.getTranformationText());
        	
            final Object payload = getPayload(message);
            final ValidationHandler validationHandler;
            final Source source;
            final Result result;
            // If the payload is a SourceResult than use its source and result.
            if (payload instanceof SourceResult)
            {
                validationHandler = null;
                final SourceResult sourceResult = (SourceResult)payload;
                source = sourceResult.getSource();
                result = sourceResult.getResult();
            }
            else
            {
            	validationHandler = new ValidationHandler(failOnWarning);
                source = SourceFactory.getInstance().createSource(payload, validationFactory, validationHandler);
                result = ResultFactory.getInstance().createResult(transformerConfig.getResultType());
            }
            
            // Perform the transformation.
            transform(source, result, adpFlowAction.getTranformationText());
            
            // Check for validation errors.
            if (validationHandler != null)
            {
            	validationHandler.check();
            }
            
            // Get the result and set on the message object
            final Object object = ResultFactory.getInstance().extractResult(result, transformerConfig.getResultType());
            
            return setPayload(message, object);
        } 
        catch (final TransformerConfigurationException e)
        {
            throw new ActionProcessingException(e.getMessage(), e);
        } 
        catch (SAXException e)
        {
        	throw new ActionProcessingException(e.getMessage(), e);
        }
        catch (ParserConfigurationException e)
        {
        	throw new ActionProcessingException(e.getMessage(), e);
        }
        catch (TransformerException e)
        {
            throw new ActionProcessingException(e.getMessage(), e);
        }
    }
    
    //By Esb-Adaptative. Initialize template file dinamic, on demand
    private synchronized void initializeTemplateFile(final String contentFile) throws ActionProcessingException {
    	try
    	{
    		String keyFile = StringUtils.createChecksum(contentFile);
    		timeStampTemplates.put(keyFile, new Date());
    		//if not exist template create
    		if(!mapTransformers.containsKey(keyFile)){
		    	final TransformerFactory factory = TransformerFactory.newInstance();
		        addFeatures(transformerConfig.getFeatures(), factory);
		        addAttributes(transformerConfig.getAttributes(), factory);
		        setResolver(transformerConfig.getUriResolver(), factory);
		        setErrorListener(new TransformerListener(failOnWarning), factory);
		        createValidationFactory(factory);
		        Templates xslTemplate = createTemplate(contentFile, factory);
		        mapTemplates.put(keyFile, xslTemplate);
		        
		        ArrayBlockingQueue<Transformer>  transformers = new ArrayBlockingQueue<Transformer>(maxTranformerPerTemplate);
		        for (int i = maxTranformerPerTemplate; --i>=0 ;)
		        {
		            transformers.add(xslTemplate.newTransformer());
		        }
		        
		        mapTransformers.put(keyFile, transformers);
    		}
    	}
    	catch (Exception e)
        {
            throw new ActionProcessingException(e.getMessage(), e);
        }
    }
    

    private void transform (final Source source, final Result result, final String contentFile) throws TransformerException, ActionProcessingException
    {
        Transformer transformer = null;
        String keyFile = null;
        try
        {
        	keyFile = StringUtils.createChecksum(contentFile);
            transformer = mapTransformers.get(keyFile).take();
        }
        catch (final Exception e)
        {
            throw new ActionProcessingException(e.getMessage(), e);
        }
        
        try
        {
            transformer.transform(source, result);
        }
        finally
        {
            try
            {
                transformer.reset();
                mapTransformers.get(keyFile).put(transformer);
            }
            catch (final InterruptedException e)
            {
                throw new ActionProcessingException(e.getMessage(), e);
            }
        }
    }
    
    private void addFeatures(final Map<String, Boolean> features, final TransformerFactory factory) throws TransformerConfigurationException
    {
        for (Entry<String, Boolean> entry : features.entrySet())
        {
            factory.setFeature(entry.getKey(), entry.getValue());
        }
    }

    private void addAttributes(final Map<String, Object> attributes, final TransformerFactory factory) throws TransformerConfigurationException
    {
        for (Entry<String, Object> entry : attributes.entrySet())
        {
            factory.setAttribute(entry.getKey(), entry.getValue());
        }
    }

    private void setResolver(final URIResolver uriResolver, final TransformerFactory factory)
    {
        if (uriResolver != null)
        {
            factory.setURIResolver(uriResolver);
        }
    }

    private void setErrorListener(final ErrorListener errorListener, final TransformerFactory factory)
    {
        factory.setErrorListener(errorListener);
    }
        
    private void createValidationFactory(final TransformerFactory factory) throws ActionLifecycleException
    {
    	if (factory.getFeature(SAXSource.FEATURE))
    	{
    		validationFactory = SAXParserFactory.newInstance();
    		validationFactory.setNamespaceAware(true);
    		// The code above enforces well-formed XML input, as described in JBESB-3036.
    		// The code below conditionally enforces valid XML input, as described in JBESB-3068.
    		if (validation)
    		{
    			validationFactory.setValidating(true);
    			if (!XMLConstants.XML_DTD_NS_URI.equals(schemaLanguage) && !XMLConstants.NULL_NS_URI.equals(schemaLanguage))
    			{
    				// W3C XML Schema or RELAX NG validation
    				for (String name : new String[]{"http://xml.org/sax/features/validation/schema", "http://apache.org/xml/features/validation/schema"})
    				{
    					try
    					{
    						validationFactory.setFeature(name, true);
    					}
    					catch (ParserConfigurationException ignored) {}
    					catch (SAXNotRecognizedException ignored) {}
    					catch (SAXNotSupportedException ignored) {}
    				}
    				if (schemaFile != null)
    				{
    					SchemaFactory schemaFactory = SchemaFactory.newInstance(schemaLanguage != null ? schemaLanguage : XMLConstants.W3C_XML_SCHEMA_NS_URI);
    					try
    					{
    						Schema schema = schemaFactory.newSchema(new StreamSource(ClassUtil.getResourceAsStream(schemaFile, getClass())));
    						validationFactory.setSchema(schema);
    					}
    					catch (SAXException e)
    					{
    						throw new ActionLifecycleException("problem creating schema '" + schemaFile + "'", e);
    					}
    				}
    			}
    			// else { DTD validation }
    		}
    	}
    	else
    	{
    		logger.log(Level.WARNING, "TransformerFactory does not support {0}", SAXSource.FEATURE);
    	}
    }

    private Templates createTemplate(final String templateContent, final TransformerFactory factory) throws ActionLifecycleException, TransformerConfigurationException
    {
    	StringReader reader = new StringReader(templateContent);
        try
        {
            return factory.newTemplates(new StreamSource(reader));
        } 
        finally
        {
            if (reader != null)
            {
                reader.close();
            }
        }
    }

    /**
     * Parses the passed-in ESB {@link ConfigTree} and populates a {@link TransformerFactoryConfig}.
     * 
     * @param config The ESB {@link ConfigTree}.
     * @return {@link TransformerFactoryConfig}.
     * @throws ConfigurationException
     */
    private TransformerFactoryConfig createConfig(final ConfigTree config) throws ConfigurationException
    {
        final Builder builder = new TransformerFactoryConfig.Builder(config.getAttribute("templateFile", ""));
        extractFeatures(config, builder);
        extractAttributes(config, builder);
        createUrlResolver(config, builder);
        builder.resultType(ResultType.valueOf(config.getAttribute("resultType", ResultType.STRING.name())));
        return builder.build();
    }

    /**
     * Extracts the factory attributes and adds them to the builder.
     * 
     * @param config The ESB {@link ConfigTree}.
     * @param builder The {@link TransformerFactoryConfig.Builder}.
     * @return Builder To support method chaining.
     */
    void extractAttributes(final ConfigTree config, final Builder builder)
    {
        for (final String attrName : config.getAttributeNames())
        {
            int idx = attrName.indexOf("factory.attribute.");
            if (idx != -1)
            {
                final Object value = config.getAttribute(attrName);
                final String name = attrName.substring(idx + "factory.attribute.".length());
                builder.attribute(name, value);
            }
        }
    }

    /**
     * Extracts the 'uriResolver' attribute from the ESB {@link ConfigTree} and instantiates a class
     * of that type. This class will be set on the passed-in builder.
     * @param config The ESB {@link ConfigTree}.
     * @param builder The {@link TransformerFactoryConfig.Builder}.
     * @throws ConfigurationException If the class could not be created.
     */
    void createUrlResolver(final ConfigTree config, final Builder builder) throws ConfigurationException
    {
        final String className = config.getAttribute("uriResolver");
        if (className != null)
        {
            try
            {
                final Class<?> resolver = ClassUtil.forName(className, getClass());
                URIResolver uriResolver = (URIResolver) resolver.newInstance();
                builder.uriResolver(uriResolver);
            } 
            catch (final ClassNotFoundException e)
            {
                throw new ConfigurationException(e.getMessage(), e);
            } 
            catch (InstantiationException e)
            {
                throw new ConfigurationException(e.getMessage(), e);
            } 
            catch (IllegalAccessException e)
            {
                throw new ConfigurationException(e.getMessage(), e);
            }
        }
    }

    /**
     * Extracts the factory features and adds them to the builder.
     * 
     * @param config The ESB {@link ConfigTree}.
     * @param builder The {@link TransformerFactoryConfig.Builder}.
     */
    void extractFeatures(final ConfigTree config, Builder builder)
    {
        for (final String attrName : config.getAttributeNames())
        {
            int idx = attrName.indexOf("factory.feature.");
            if (idx != -1)
            {
                final String value = config.getAttribute(attrName);
                final String name = attrName.substring(idx + "factory.feature.".length());
                builder.feature(name, Boolean.valueOf(value));
            }
        }
    }

    private Object getPayload(final Message message) throws ActionProcessingException
    {
        try
        {
            return payloadProxy.getPayload(message);
        } 
        catch (MessageDeliverException e)
        {
            throw new ActionProcessingException(e.getMessage(), e);
        }
    }
    
    private Message setPayload(final Message message, final Object payload) throws ActionProcessingException
    {
        try
        {
            payloadProxy.setPayload(message, payload);
        } 
        catch (MessageDeliverException e)
        {
            throw new ActionProcessingException(e.getMessage(), e);
        }
        return message;
    }
    
    public TransformerFactoryConfig getTranformerConfig()
    {
        return transformerConfig;
    }

    @Override
    public String toString()
    {
        return String.format("%s templateFile=%s, validation=%b, schemaFile=%s, schemaLanguage=%s, failOnWarning=%b, features=%s, attributes=%s", getClass().getSimpleName(), transformerConfig.getTemplateFile(), validation, schemaFile, schemaLanguage, failOnWarning, transformerConfig.getFeatures(), transformerConfig.getAttributes());
    }
    
    private static class TransformerListener implements ErrorListener
    {
        private final boolean failOnWarning;

        public TransformerListener(boolean failOnWarning) 
        {
            this.failOnWarning = failOnWarning;
        }

        public void warning(TransformerException exception) throws TransformerException 
        {
            if (failOnWarning) 
            {
                throw exception;
            } 
            else 
            {
                logger.log(Level.WARNING, "Transformation Warning.", exception);
            }
        }

        public void error(TransformerException exception) throws TransformerException 
        {
            throw exception;
        }

        public void fatalError(TransformerException exception) throws TransformerException 
        {
            throw exception;
        }
    }
    
    private static class ValidationHandler implements ErrorHandler
    {
        private final boolean failOnWarning;
        private SAXParseException exception = null;

        public ValidationHandler(boolean failOnWarning) 
        {
            this.failOnWarning = failOnWarning;
        }
        
        private void check() throws SAXParseException
        {
        	if (exception != null)
        	{
        		throw exception;
        	}
        }
        
    	public void warning(SAXParseException exception) throws SAXException
    	{
    		if (failOnWarning)
    		{
    			this.exception = exception;
    			throw exception;
    		}
    		else
    		{
    			logger.log(Level.WARNING, "Validation Warning.", exception);
    		}
    	}
    	
    	public void error(SAXParseException exception) throws SAXException
    	{
    		this.exception = exception;
    		throw exception;
    	}
    	
    	public void fatalError(SAXParseException exception) throws SAXException
    	{
    		this.exception = exception;
    		throw exception;
    	}
    }

    public int getNumberOfPooledTransfomers ()
    {
        if (mapTransformers == null)
            throw new IllegalStateException("The transformers have not been initialized yet. Please make sure that initialize has bee called prior to calling this method.");
        
        return maxTranformerPerTemplate;
    }
    
    /**
     * If the aggregation process is complete then return true. This depends on the configuration. 
     * 
     * @param splitterTimeStamp The splitter timestamp.
     * @return True if the message is timed out, otherwise false.
     */
    private boolean isTimedOut(long splitterTimeStamp){
        long now = new Date().getTime();
        long expiration = splitterTimeStamp + 3600000L;

        if (now > expiration) {
            return true;
        }
        
        return false;
    }
    
    /**
     * Check response timeout
     */
    class TimeoutChecker extends Thread {
        private final Lock terminateLock = new ReentrantLock() ;
        private final Condition terminateCondition = terminateLock.newCondition() ;
        private boolean terminated ;

        public void run() {
            boolean running = true;
            
            while(running) {
            	synchronized(timeStampTemplates) {
            		Iterator<Entry<String, Date>> it = timeStampTemplates.entrySet().iterator();
            		while (it.hasNext()){
            			Entry<String, Date> entry = it.next();
            			if (isTimedOut(entry.getValue().getTime())) {
            				it.remove();
            			}
            		}
            	}
                terminateLock.lock() ;
                try {
                    if (!terminated) {
                        terminateCondition.await(5, TimeUnit.MINUTES) ;
                    }
                    running = !terminated ;
                } catch (InterruptedException ie){
                } finally {
                    terminateLock.unlock() ;
                }
            }
        }

        public void terminate() {
            terminateLock.lock() ;
            try{
                terminated = true ;
                terminateCondition.signalAll() ;
            }
            finally{
                terminateLock.unlock() ;
            }
        }
    }
}
